package scene.shape.geometry;
import raytracing.HitRecord;
import raytracing.Ray;
import scene.shape.BoundingBox;
import util.Axis;
import util.Matrix4f;
import util.TexCoord2f;
import util.Vector3f;
import util.Vector4f;

/**
 * Represents a triangle face
 * @author glenn
 *
 */
public class Triangle extends Primitive{
	
	/**
	 * The 3 vertices of the triangle
	 */
	public Vector3f v1,v2,v3;
	public Vector3f n1,n2,n3;
	public TexCoord2f t1,t2,t3;
	
	public Vector3f center;
	private float a,b,c,d,e,f;
	
	public Triangle(Vector3f v1, Vector3f v2, Vector3f v3,
			Vector3f n1, Vector3f n2, Vector3f n3, TexCoord2f t1, TexCoord2f t2, TexCoord2f t3){
		this.v1 = v1;
		this.v2 = v2;
		this.v3 = v3;
		this.n1 = n1;
		this.n2 = n2;
		this.n3 = n3;
		this.t1 = t1;
		this.t2 = t2;
		this.t3 = t3;
		this.center = v1.scale(1/3).add(v2.scale(1/3)).add(v3.scale(1/3));
		precalc();
	}
	
	public Triangle(Vector3f v1, Vector3f v2, Vector3f v3,
			Vector3f n1, Vector3f n2, Vector3f n3){
		this.v1 = v1;
		this.v2 = v2;
		this.v3 = v3;
		this.n1 = n1;
		this.n2 = n2;
		this.n3 = n3;
		this.center = v1.scale(1/3).add(v2.scale(1/3)).add(v3.scale(1/3));
		precalc();
	}
	
	public Triangle(Vector3f a, Vector3f b, Vector3f c){
		this.v1 = a;
		this.v2 = b;
		this.v3 = c;
		this.center = v1.scale(1/3).add(v2.scale(1/3)).add(v3.scale(1/3));
		precalc();
	}
	
	private void precalc(){ //precalc for faster performance
		  a = this.v1.x - this.v2.x;
		  b = this.v1.y - this.v2.y;
		  c = this.v1.z - this.v2.z;
		  d = this.v1.x - this.v3.x;
		  e = this.v1.y - this.v3.y;
		  f = this.v1.z - this.v3.z;
	}
	
	/**
	 * Checks for intersection
	 * @param r the ray to intersect this triangle with
	 * @param mint the minimum t allowed, any t below this value will not be considered as a valid hit
	 * @param maxt the maximum t allowed, any t above this value will not be considered as a valid hit
	 * @return t the value of t, the fractional distance along the ray at which the intersection occurs
	 * 			if there is no intersection, or t does not fall in the interval [mint, maxt] Float.NaN is returned
	 */
	public HitRecord intersects(Ray r, float mint, float maxt){
		Matrix4f invTransform = shape.getInverseTransform();
		Ray oray = new Ray(invTransform.transform(r.getE(),1), invTransform.transform(r.getS(),1));
		float g = oray.getDirection().x,
			  h = oray.getDirection().y,
			  i = oray.getDirection().z,
			  j = this.v1.x - oray.getOrigin().x,
			  k = this.v1.y - oray.getOrigin().y,
			  l = this.v1.z - oray.getOrigin().z,
			  eiminushf = e * i - h * f,
			  gfminusdi = g * f - d * i,
			  dhminuseg = d * h - e * g,
			  akminusjb = a * k - j * b,
			  blminuskc = b * l - k * c,
			  jcminusal = j * c - a * l,
			  m = a * eiminushf + b * gfminusdi + c * dhminuseg;
		float t = - (f * akminusjb + e * jcminusal + d * blminuskc) / m;
		if(t < mint || t > maxt)
			return HitRecord.NOHIT;
		float  gamma = (i * akminusjb + h * jcminusal + g * blminuskc) / m;
		if(gamma < 0 || gamma > 1)
			return HitRecord.NOHIT;
		float beta = (j * eiminushf + k * gfminusdi + l * dhminuseg) / m;
		if(beta < 0 || beta > (1 - gamma))
			return HitRecord.NOHIT;
		return new HitRecord(true, t, r, shape.getInverseTransformTranspose().transform(getWeightedNormal(1-beta-gamma,beta,gamma),0), this, beta, gamma);
	}

	public Vector3f getAverageNormal(){
		if(n1 == null && n2 == null && n3 == null){
			return getSurfaceNormal();
		}
		int numberOfNormals = 0;
		float nx = 0, ny = 0, nz = 0;
		if(n1!=null){
			numberOfNormals++;
			nx += n1.x;
			ny += n1.y;
			nz += n1.z;
		}
		if(n2 != null){
			numberOfNormals++;
			nx += n2.x;
			ny += n2.y;
			nz += n2.z;
		}
		if(n3 != null){
			numberOfNormals++;
			nx += n3.x;
			ny += n3.y;
			nz += n3.z;
		}
		nx = nx / numberOfNormals;
		ny = ny / numberOfNormals;
		nz = nz / numberOfNormals;
		return shape.getInverseTransform().transpose().multiply(new Vector4f(nx,ny,nz,0)).get3DVector();		
	}

	public Vector3f getSurfaceNormal() {
		return v2.subtract(v1).cross(v3.subtract(v1).normalize());
	}
	
	/**
	 * Weighted normal, voor phong normaal interpolatie
	 * @param alpha
	 * @param beta
	 * @param gamma
	 * @return
	 */
	public Vector3f getWeightedNormal(float alpha, float beta, float gamma){
		if(n1 == null && n2 == null && n3 == null){
			return (v1.subtract(v2).cross(v1.subtract(v3).normalize()));
		}
		Vector3f normal = new Vector3f(0,0,0);
		if(n1!=null){
			normal = normal.add(n1.scale(alpha));
		}
		if(n2 != null){
			normal = normal.add(n2.scale(beta));
		}
		if(n3 != null){
			normal = normal.add(n3.scale(gamma));
		}
		return normal.normalize();	
	}

	public void transform(Matrix4f transformation) {
		v1 = new Vector3f(transformation.multiply(new Vector4f(v1,1)));
		v2 = new Vector3f(transformation.multiply(new Vector4f(v2,1)));
		v3 = new Vector3f(transformation.multiply(new Vector4f(v3,1)));
		Matrix4f normalTransform = transformation.toNormalTransform();
		if(n1 != null)
			n1 = new Vector3f(normalTransform.multiply(new Vector4f(n1,0)));
		if(n2 != null)
			n2 = new Vector3f(normalTransform.multiply(new Vector4f(n2,0)));
		if(n3 != null)
			n3 = new Vector3f(normalTransform.multiply(new Vector4f(n3,0)));
		precalc();
	}
	
	public float getZ(float alpha, float beta, float gamma){
		return alpha * v1.z + beta * v2.z + gamma * v3.z;
	}
	
	public Vector3f getPointAt(float alpha, float beta, float gamma){
		return v1.scale(alpha).add(v2.scale(beta)).add(v3.scale(gamma));
	}
	
	/**
	 * Return boundingbox in world space.
	 */
	@Override
	public BoundingBox getBoundingBox() {
		Vector3f v1 = shape.getTransform().transform(this.v1,1),
				 v2 = shape.getTransform().transform(this.v2,1),
				 v3 = shape.getTransform().transform(this.v3,1);
		float
			minx = Math.min(v1.x, Math.min(v2.x, v3.x)),
			maxx = Math.max(v1.x, Math.max(v2.x, v3.x)),
			miny = Math.min(v1.y, Math.min(v2.y, v3.y)),
			maxy = Math.max(v1.y, Math.max(v2.y, v3.y)),
			minz = Math.min(v1.z, Math.min(v2.z, v3.z)),
			maxz = Math.max(v1.z, Math.max(v2.z, v3.z));
		return new BoundingBox(new Vector3f(minx,miny,minz), new Vector3f(maxx,maxy,maxz));
	}
	
	@Override
	public Vector3f getCenter(){
		return v1.scale(1/3f).add(v2.scale(1/3f)).add(v3.scale(1/3f));
	}
	
	public float getExtreme(Axis axis, boolean left){
        float a, b, c;
        switch (axis){
            case X: a = v1.x; b = v2.x; c = v3.x; break;
            case Y: a = v1.y; b = v2.y; c = v3.y; break;
            case Z: a = v1.z; b = v2.z; c = v3.z; break;
            default: assert false; return 0;
        }
        if (left){
            if (a < b){
                if (a < c)
                    return a;
                else
                    return c;
            }else{
                if (b < c)
                    return b;
                else
                    return c;
            }
        }else{
            if (a > b){
                if (a > c)
                    return a;
                else
                    return c;
            }else{
                if (b > c)
                    return b;
                else
                    return c;
            }
        }
    }

	@Override
	public float getU(HitRecord hr) {
		if(t1 == null || t2 == null || t3 == null)
			return 0;
		float beta = hr.getBeta(), gamma = hr.getGamma(), alpha = (1-beta-gamma);
		return alpha * t1.x + beta * t2.x + gamma * t3.x; 
	}

	@Override
	public float getV(HitRecord hr) {
		if(t1 == null || t2 == null || t3 == null)
			return 0;
		float beta = hr.getBeta(), gamma = hr.getGamma(), alpha = (1-beta-gamma);
		return alpha * t1.y + beta * t2.y + gamma * t3.y; 
	}

	@Override
	public HitRecord hit(Ray r, float mint, float maxt) {
		return intersects(r, mint, maxt);
	}

	public float area() {
		Vector3f AB = v2.subtract(v1), AC = v3.subtract(v1);
		return 0.5f * (AB.cross(AC)).length();
	}

}
